{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Предсказание рейтинга фильма"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Описан процесс сбора данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данные взяты из <a href=\"https://www.imdb.com/interfaces/\">IMDb</a> 2 апреля 2018 года. Источник обновляет данные ежедневно. Если вы желаете взять именно те данные, которые использованы в этой работе, скачать их можно <a href=\"https://yadi.sk/d/m1aE5pkQ3UeVpJ\">здесь</a> (~3 Gb)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Подробное описание решаемой задачи, ее ценности"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Перед моделью стоит задача регрессии — предсказать рейтинг фильма на основе данных о нем.<br/>\n",
    "Напрямую модель можно использовать в том случае, когда у нового фильма (по вашей оценке) пока недостаточно голосов для получения достоверного рейтинга, но уже известны значения признаков. Или в том случае, когда вы только собираете команду для создания фильма из известных в индустрии профессионалов, а так же выбираете название фильма :)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Описание целевого и прочих признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Каждый датасет хранится в .tsv формате в UTF-8 кодировке. Первая строка — заголовки. Символ `'\\N'` используется для обозначения отсутствующей информации в определенном поле.\n",
    "\n",
    "Список датасетов и описание полей в них ниже. Здесь и далее под произведениями я понимаю полнометражные и короткометражные фильмы, сериалы, видео и другие типы контента, опубликованного на IMDb.\n",
    "\n",
    "title.basics.tsv.gz - содержит следующую информацию о произведениях:\n",
    "\n",
    "    tconst (string) - численно-буквенный уникальный идентификатор произведения\n",
    "    titleType (string) – тип/формат произведения (например, 'movie', 'short', 'tvseries' и т. д.)\n",
    "    primaryTitle (string) – наиболее популярное название / название, использованное авторами в промо-материалах релиза\n",
    "    originalTitle (string) - оригинальное название на оригинальном языке\n",
    "    isAdult (boolean) - 0: для всех; 1: только для взрослых\n",
    "    startYear (YYYY) – год релиза. Для сериалов это год релиза первой серии\n",
    "    endYear (YYYY) – год последней серии (используется для сериалов). ‘\\N’ для всех остальных типов/форматов\n",
    "    runtimeMinutes – продолжительность в минутах\n",
    "    genres (string array) – до трех (включительно) жанров, соответствующих произведению\n",
    "\n",
    "title.crew.tsv.gz – информация о режиссерах и сценаристах:\n",
    "\n",
    "    tconst (string) - численно-буквенный уникальный идентификатор произведения\n",
    "    directors (array of nconsts) - режиссер(ы)\n",
    "    writers (array of nconsts) – сценарист(ы)\n",
    "\n",
    "title.principals.tsv.gz – информация о командах, участвующих в создании произведений:\n",
    "\n",
    "    tconst (string) - численно-буквенный уникальный идентификатор произведения\n",
    "    principalCast (array of nconsts) – наиболее высокооплачиваемые участники\n",
    "\n",
    "title.ratings.tsv.gz – рейтинг IMDb и количество голосов:\n",
    "\n",
    "    tconst (string) - численно-буквенный уникальный идентификатор произведения\n",
    "    averageRating – взвешенное среднее индивидуальных голосов пользователей\n",
    "    numVotes - количество голосов"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Чтение и предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Импорт библиотек"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "%matplotlib inline\n",
    "\n",
    "from scipy.stats import normaltest, skewtest, probplot, boxcox\n",
    "from scipy.sparse import hstack, vstack\n",
    "\n",
    "from sklearn.model_selection import train_test_split,\\\n",
    "                                    GridSearchCV,\\\n",
    "                                    KFold,\\\n",
    "                                    validation_curve,\\\n",
    "                                    learning_curve\n",
    "from sklearn.preprocessing import StandardScaler, LabelEncoder, OneHotEncoder\n",
    "from sklearn.feature_extraction.text import CountVectorizer\n",
    "from sklearn.linear_model import SGDRegressor\n",
    "from sklearn.metrics import mean_squared_error"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Чтение данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PATH_TO_DATA = './data/'\n",
    "\n",
    "titles_df = pd.read_csv(PATH_TO_DATA + 'title.basics.tsv/data.tsv',\n",
    "                              sep='\\t',\n",
    "                              dtype=str,\n",
    "                              na_values=['\\\\N'])\n",
    "\n",
    "ratings_df = pd.read_csv(PATH_TO_DATA + 'title.ratings.tsv/data.tsv',\n",
    "                         sep='\\t',\n",
    "                         dtype=str,\n",
    "                         na_values=['\\\\N'])\n",
    "\n",
    "crews_df = pd.read_csv(PATH_TO_DATA + 'title.crew.tsv/data.tsv',\n",
    "                      sep='\\t',\n",
    "                      dtype=str,\n",
    "                      na_values=['\\\\N'])\n",
    "\n",
    "principals_df = pd.read_csv(PATH_TO_DATA + 'title.principals.tsv/data.tsv',\n",
    "                            sep='\\t',\n",
    "                            dtype=str,\n",
    "                            na_values=['\\\\N'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Предобработка данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Уберем все объекты, у которых нет нужных данных.\n",
    "titles_df.drop(\n",
    "    index=titles_df[(titles_df['originalTitle'].isnull()) |\\\n",
    "                    (titles_df['startYear'].isnull()) |\\\n",
    "                    (titles_df['genres'].isnull()) |\\\n",
    "                    (titles_df['runtimeMinutes'].isnull())\n",
    "                   ].index,\n",
    "    axis=0,\n",
    "    inplace=True)\n",
    "# И уберем лишние данные\n",
    "titles_df.drop('originalTitle', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Заполняю пропуски, привожу к нужным типам.\n",
    "titles_df['isAdult'] = titles_df['isAdult'].astype(int)\n",
    "titles_df['endYear'].fillna(0, inplace=True)\n",
    "titles_df['startYear'] = titles_df['startYear'].astype(int)\n",
    "titles_df['runtimeMinutes'] = titles_df['runtimeMinutes'].astype(int)\n",
    "\n",
    "ratings_df['averageRating'] = ratings_df['averageRating'].astype(float)\n",
    "ratings_df['numVotes'] = ratings_df['numVotes'].astype(int)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Делаем аналог INNER JOIN для данных по фильму и рейтингам фильмов.\n",
    "df = pd.merge(titles_df, ratings_df, how='inner', on='tconst')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "# Составляю список уникальных жанров, он нам понадобится позже.\n",
    "unique_genres = set(','.join(df['genres']).split(','))\n",
    "\n",
    "# А это ручной аналог OHE, без необходимости использовать Label Encoder.\n",
    "# Довольно долго вычисляет (~12 секунд), т. к. проходим по всей выборке\n",
    "# столько раз, сколько у нас жанров. Но с данными в таком виде\n",
    "# мне было проще работать в дальнейшем.\n",
    "\n",
    "for curr_genre in unique_genres:\n",
    "    df[curr_genre] = df['genres'].map(\n",
    "        lambda r: 1 if curr_genre in r.split(',') else 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Опять делаем аналог INNER JOIN,\n",
    "# на этот раз — с данными о режиссерах и сценаристах.\n",
    "df = pd.merge(df, crews_df, how='inner', on='tconst')\n",
    "\n",
    "# Заполняем пропуски.\n",
    "df['directors'].fillna('', inplace=True)\n",
    "df['writers'].fillna('', inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# В principals_df уникальный идентификатор произведения может\n",
    "# встречаться много раз, и каждый раз с новым участником.\n",
    "# Объединяю их всех в одну строку через запятую.\n",
    "principals_by_title_df =\\\n",
    "    principals_df[['tconst',\n",
    "                   'nconst']].groupby('tconst').agg(','.join).reset_index()\n",
    "\n",
    "# И снова делаю INNER JOIN с этими данными.\n",
    "df = pd.merge(df, principals_by_title_df, how='inner', on='tconst')\n",
    "df = df.rename({'nconst': 'principals'}, axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Первичный анализ данных, включая визуальный"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Типы признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим, с какими признаками мы имеем дело.\n",
    "В `df` присутствуют:\n",
    "\n",
    "- 31 категориальный признак (`[tconst, titleType, isAdult]` и 28 жанров)\n",
    "- 5 количественных признаков, считая целевой (`[startYear, endYear, runtimeMinutes, numVotes, averageRating]`)\n",
    "- 3 признака, содержащие тексты (`[primaryTitle, directors, writes]`). Их с помощью OHE я переведу в разреженную матрицу с категориальными признаками."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Пропуски в данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "missing_values_count = df.isnull().sum()\n",
    "\n",
    "print(\"Из {} объектов данные пропущены у {} объектов.\\n\\\n",
    "Потому что выше я заполнил пропуски, а так же убрал объекты \\\n",
    "без минимально необходимого набора данных.\\nСчитаю это возможным, \\\n",
    "т. к. данных все равно осталось более полумиллиона.\"\n",
    "      .format(df.shape[0], missing_values_count.sum()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Данные о годе последней серии указаны у {} произведений\"\n",
    "      .format(df[df['endYear']!=0]['tconst'].count()))\n",
    "\n",
    "print(\"Данные о режиссерах отсутствуют у {} произведений\"\n",
    "      .format(df[df['directors']=='']['tconst'].count()))\n",
    "\n",
    "print(\"Данные о сценаристах отсутствуют у {} произведений\"\n",
    "      .format(df[df['writers']=='']['tconst'].count()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Выбросы в данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.loc[:, [dc for dc in df.columns if dc not in list(unique_genres)]].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что потенциальные кандидаты на отнесение к выбросам находятся в колонках `runtimeMinutes` и `numVotes`. Посмотрим на них поближе."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.subplot(1, 2, 1)\n",
    "sns.boxplot(df['runtimeMinutes'], orient='v')\n",
    "plt.yscale('log')\n",
    "plt.subplot(1, 2, 2)\n",
    "sns.boxplot(df['numVotes'], orient='v')\n",
    "plt.yscale('log')\n",
    "plt.tight_layout();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Действительно, объекты, которые можно посчитать выбросами, явно присутствуют. Посмотрим, сколько их."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[['runtimeMinutes', 'numVotes']].describe(percentiles=[0.8, 0.95, 0.99, 0.999])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Они составляют меньше процента. Посмотрим, является ли это ошибкой или такова природа данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.sort_values(by=['runtimeMinutes'],\n",
    "               ascending=False\n",
    "              ).iloc[:5].loc[:, [dc for dc in df.columns if dc not in list(unique_genres)]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Начнем по порядку, с <a href=\"https://www.imdb.com/title/tt2659636/\" target=\"_blank\">Modern Times Forever (tt2659636)</a>. Как видно, эта картина действительно длится `240h`, или другими словами 14400 / 60 = 240 часов.\n",
    "\n",
    "Второе в этом списке произведение — <a href=\"https://www.imdb.com/title/tt2355497/\" target=\"_blank\">Beijing 2003 (tt2355497)</a>:\n",
    ">Runtime approx. 150 hours and one of the longest movies ever made.\n",
    "\n",
    "Думаю, можно сделать вывод, что таково реальное распределение данных. Поэтому удалять эти объекты не планирую."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.sort_values(by=['numVotes'],\n",
    "               ascending=False\n",
    "              ).iloc[:5].loc[:, [dc for dc in df.columns if dc not in list(unique_genres)]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Первым в списке является известный фильм <a href=\"https://www.imdb.com/title/tt0111161/\" target=\"_blank\">The Shawshank Redemption (tt0111161)</a>. 2 апреля 2018, когда я скачал выгрузку данных, он имел 1,933,861 голосов. С тех пор на сегодня, 19 апреля 2018, проголосовало еще несколько тысяч человек, общее количество — 1,940,993 голосов.\n",
    "\n",
    "С небольшим отрывом идет фильм <a href=\"https://www.imdb.com/title/tt0468569/\" target=\"_blank\">The Dark Knight (tt0468569)</a>, 1,904,272 голосов на момент скачивания и 1,911,673 на момент проверки.\n",
    "\n",
    "По названиям следующих фильмов искушенный зритель может сделать предположение, что количество голосов является скорее реальностью, чем ошибкой в данных. Поэтому исключать эти фильмы из выборки не планирую."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Матрица корреляций для количественных признаков"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.heatmap(df[['startYear',\n",
    "                'endYear',\n",
    "                'runtimeMinutes',\n",
    "                'numVotes',\n",
    "                'averageRating']].corr(method='pearson'),\n",
    "            square=True,\n",
    "            annot=True,\n",
    "            fmt=\".2f\")\n",
    "plt.title('Корреляция признаков по Пирсону');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что у количественных признаков практически отсутствует корреляция с таргетом.\n",
    "\n",
    "Хотя можно попробовать использовать год релиза и продолжительность в текущем виде или разбить их на категории."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(20, 5))\n",
    "\n",
    "plt.subplot(1, 3, 1)\n",
    "genres_count = df[list(unique_genres)].sum().reset_index()\n",
    "sns.barplot(genres_count['index'], genres_count[0])\n",
    "plt.xticks(rotation=90)\n",
    "plt.title('Количество объектов с разными жанрами')\n",
    "plt.xlabel('genre')\n",
    "plt.ylabel('count')\n",
    "\n",
    "plt.subplot(1, 3, 2)\n",
    "sns.countplot(df['titleType'])\n",
    "plt.xticks(rotation=90)\n",
    "plt.title('Количество объектов с разными titleType')\n",
    "\n",
    "plt.subplot(1, 3, 3)\n",
    "sns.countplot(df['isAdult'])\n",
    "plt.title('Количество объектов с разными isAdult');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Больше всего фильмов имеют жанры Drama и Comedy. С отставанием следуют короткометражки.\n",
    "\n",
    "Большую часть объектов составляют сериалы, полнометражные фильмы и короткометражки.\n",
    "\n",
    "И большая часть фильмов не отмечена как \"для взрослых\" (признак isAdult)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Исследование распределения целевого признака (в том числе стат-тесты на нормальность и скошенность распределения)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.distplot(df['averageRating'], bins=22)\n",
    "plt.title('Распределение целевого признака')\n",
    "plt.xlabel('Average Rating');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тест на нормальность распределения целевого признака"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "alpha = 1e-2\n",
    "\n",
    "k2, p = normaltest(df['averageRating'])\n",
    "print(\"p = {:.3f}\".format(p))\n",
    "\n",
    "if p < alpha:  # нулевая гипотеза: распределение является нормальным\n",
    "    print(\"Нулевая гипотеза может быть отклонена\")\n",
    "else:\n",
    "    print(\"Нулевая гипотеза НЕ может быть отклонена\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тест на скошенность распределения целевого признака"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "skewtest(df['averageRating'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что распределение нельзя назвать нормальным. Посмотрим, как выглядит соответствующий график."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(6, 5))\n",
    "probplot(df['averageRating'], dist=\"norm\", plot=plt);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для задачи регрессии желательно привести распределение к нормальному. Попробуем это сделать. Начнем с логарифмирования."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "probplot(np.log(df['averageRating']), dist=\"norm\", plot=plt);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.distplot(np.log(df['averageRating']));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Логарифмирование только ухудшило ситуацию. Поскольку мне неизвестно реальное распределение, попробую преобразование Бокса-Кокса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['bc_avg_rating'], bc_lmbda = boxcox(df['averageRating'])\n",
    "\n",
    "probplot(df['bc_avg_rating'], dist=\"norm\", plot=plt);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Такое преобразование заметно помогло. Посмотрим на численную оценку."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "alpha = 1e-3\n",
    "\n",
    "k2, p = normaltest(df['bc_avg_rating'])\n",
    "print(\"p = {:.3f}\".format(p))\n",
    "\n",
    "if p < alpha:  # нулевая гипотеза: распределение является нормальным\n",
    "    print(\"Нулевая гипотеза может быть отклонена\")\n",
    "else:\n",
    "    print(\"Нулевая гипотеза НЕ может быть отклонена\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Распределение не удалось привести к нормальному, но все же мы сдвинулись в эту сторону.\n",
    "\n",
    "На будущее сделаем преобразование, обратное преобразованию Бокса-Кокса."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def reverse_boxcox(bc_target, bc_lmbda):\n",
    "    return np.power((bc_target * bc_lmbda) + 1, 1 / bc_lmbda)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим теперь на взаимодействия признаков с целевым.\n",
    "\n",
    "Распределение рейтинга по жанрам."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(20, 15))\n",
    "\n",
    "for idx, curr_genre in enumerate(unique_genres):\n",
    "    plt.subplot(7, 4, idx+1)\n",
    "    plt.title(curr_genre)\n",
    "    sns.distplot(df[df[curr_genre]==1]['bc_avg_rating'], bins=22)\n",
    "    plt.xlabel('Average Rating')\n",
    "\n",
    "plt.tight_layout();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Распределение рейтинга по типу контента."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(20, 5))\n",
    "\n",
    "for idx, titleType in enumerate(df['titleType'].unique()):\n",
    "    plt.subplot(2, 5, idx+1)\n",
    "    plt.title(titleType)\n",
    "    sns.distplot(df[df['titleType']==titleType]['bc_avg_rating'], bins=22)\n",
    "    plt.xlabel('Average Rating')\n",
    "\n",
    "plt.tight_layout();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Зависимость рейтинга от продолжительности контента."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(20, 10))\n",
    "\n",
    "g = sns.jointplot(np.log(df['runtimeMinutes'],\n",
    "                         where=df['runtimeMinutes']>0),\n",
    "                  df['bc_avg_rating'])\n",
    "\n",
    "g.ax_joint.set_xlabel('Log Runtime Minutes')\n",
    "g.ax_joint.set_ylabel('Average Rating');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Распределение рейтинга в зависимости от признака `isAdult`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(20, 5))\n",
    "\n",
    "sns.violinplot(df['bc_avg_rating'],\n",
    "               pd.Series([True] * df.shape[0]),\n",
    "               hue=df['isAdult'].map({0: 'No', 1: 'Yes'}),\n",
    "               split=True,\n",
    "               orient='h',\n",
    "               palette='seismic');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Почему и как можно преобразовать целевой признак"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "С учетом анализа и выводов выше считаю, что стоит работать с признаком, преобразованным по методу Бокса-Кокса."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы, инсайты, найденные зависимости"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Предположения о природе различных корреляций/пропусков/закономерностей и выбросов, найденных в предыдущих пунктах. Пояснение, почему они важны для решаемой задачи."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1. Благодаря анализу выбросов можно сделать гипотезу, что стоит взвесить количество голосов с учетом года релиза. И завести признак \"Арифметическое среднее количества голосов в год\". А вот абсолютное количество голосов исключить из данных, используемых для обучения модели.\n",
    "2. Корреляции рейтинга по Пирсону с количественными признаками я не обнаружил. Посмотрим, сработает ли гипотеза, которая интуитивно кажется мне основной — зависимость рейтинга от того, какие люди участвуют в производстве картины.\n",
    "3. Пропуски, как я описал выше, мы обработали.\n",
    "4. Выбросы, которые мне удалось найти, оказались на деле не ошибками, а реальными данными.\n",
    "\n",
    "Важность этих выводов, на мой взгляд, объясняется следующими пунктами:\n",
    "- Линейная регрессия будет плохо работать, если нет корреляций (как в нашем случае). Значит, надо поискать зависимость с другими признаками (например, кто из людей участвовал в создании фильма) и попробовать построить дополнительные признаки, чтобы учесть наличие нелинейных зависимостей.\n",
    "- Благодаря анализу была найдена такая зависимость, которая будет отражена в признаке \"Арифметическое среднее количества голосов в год\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "С учетом системы рейтингов IMDb можно было бы рассмотреть задачу как многоклассовую one-against-all классификацию (с 91 классом: \"10.0\", \"9.9\", \"9.8\" и т. д. до \"1.0\"). Однако, интересно все же решить задачу регрессии, так как полученный с помощью такой модели результат затем не составит труда округлить до нужного числа.\n",
    "\n",
    "Для регрессии я выбираю метрику MSE, среднеквадратичная ошибка. Хотя есть некоторое количество выбросов, оно все же довольно маленькое относительно всей выборки, а само распределение довольно близко к нормальному."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "В этой задаче я планирую работать с линейной моделью.\n",
    "\n",
    "Во-первых, у нас будет разреженная матрица: названия произведений, сценаристы, режиссеры, другие участники будут обработаны как токены.\n",
    "\n",
    "Во-вторых, данных довольно много и хочется по возможности оптимизировать скорость обучения, например, с помощью SGDRegressor.\n",
    "\n",
    "Как альтернативы можно было бы рассмотреть Lasso или Ridge. На основе своих прошлых экспериментов я думаю, что эти варианты будут медленнее, поэтому останавливаю свой выбор на SGDRegressor."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Для начала создадим новый признак — среднее количество голосов в год. Поскольку данные были скачаны в 2018 году, используем этот год за точку отсчета. При этом будем считать, что фильмы, выпущенные в 2018 году, собрали все голоса за один год (т. е. год релиза будем вычитать из `2019`).\n",
    "\n",
    "Пока модель не готова, нельзя сказать, насколько обоснованным является создание такого дополнительного признака. Зато ясно, что если один фильм набрал 10 голосов за 10 лет, а другой — столько же голосов за 1 год, я не хочу считать это как одинаковое количество голосов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['num_votes_yearly_avg'] = df['numVotes'] / (2019 - df['startYear'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Информацию об абсолютном количестве голосов я не буду включать в модель, а вот `startYear` оставлю. `endYear` у большинства объектов тоже отсутствует (равен нулю), так что и его пока не буду включать.\n",
    "\n",
    "Второй признак, который мне понадобится, это `titleTypeEncoded`. Я планирую использовать OHE преобразование с `titleType`, и OneHotEncoder ожидает получить числа, а не строки на вход. Поэтому предварительно надо преобразовать данные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "le = LabelEncoder()\n",
    "df['titleTypeEncoded'] = le.fit_transform(df['titleType'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Разделение на train, valid и test части"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь, когда данные собраны и еще не прошли предобработку, стоит разделить выборку на обучающую, валидационную и тестовую. Поскольку корреляция (по Пирсону) рейтинга и года релиза практически отсутствует, данные можно спокойно перемешать. А тестовую часть просто возьму с конца. Такой способ выделения тестовой выборки обоснован следующим:\n",
    "- количество объектов больше полумиллиона;\n",
    "- год релиза практически не имеет никакой корреляции с целевой переменной по Пирсону."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "TEST_SIZE = 0.2 # 20% выборки отведем для теста.\n",
    "idx_split = df.shape[0] - int(df.shape[0] * TEST_SIZE)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на получившиеся распределения целевого признака в разных частях данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_df = df.iloc[:idx_split]\n",
    "holdout = df.iloc[idx_split:]\n",
    "\n",
    "plt.figure(figsize=(12, 3))\n",
    "\n",
    "sns.distplot(train_df['bc_avg_rating'],\n",
    "             color='darkblue',\n",
    "             label='Train and Valid Parts\\' Target Distribution')\n",
    "sns.distplot(holdout['bc_avg_rating'],\n",
    "             color='darkred',\n",
    "             label='Test Part\\'s Target Distribution');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Идеально не совпадает, но примерно похоже. Теперь разделим обучающую выборку на train и valid части."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_valid, y_train, y_valid = train_test_split(\n",
    "    train_df[[\n",
    "        'titleTypeEncoded',\n",
    "        'isAdult',\n",
    "        'primaryTitle',\n",
    "        'startYear',\n",
    "        'directors',\n",
    "        'writers',\n",
    "        'principals',\n",
    "        'runtimeMinutes',\n",
    "        'num_votes_yearly_avg'\n",
    "    ] + list(unique_genres)],\n",
    "    train_df['bc_avg_rating'],\n",
    "    test_size=0.3,\n",
    "    random_state=17)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_train_complete = train_df['bc_avg_rating']\n",
    "y_holdout_true = holdout['bc_avg_rating']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Масштабирование\n",
    "Масштабировать буду количественные признаки: `runtimeMinutes`, `num_votes_yearly_avg`, `startYear`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "features_to_scale = [\n",
    "    'runtimeMinutes',\n",
    "    'num_votes_yearly_avg',\n",
    "    'startYear'\n",
    "]\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(X_train[features_to_scale])\n",
    "\n",
    "X_train_scaled = scaler.transform(X_train[features_to_scale])\n",
    "X_valid_scaled = scaler.transform(X_valid[features_to_scale])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_holdout_scaled = scaler.transform(holdout[features_to_scale])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### OHE\n",
    "OneHotEncode преобразование планирую только для `titleType`. Для `isAdult` оно не требуется, т. к. это и так бинарный признак. Для жанров тоже не требуется, потому что я их уже преобразовал ранее. И все же я включу эти колонки в текущее преобразование, потому что так мне удобнее с ними работать дальше."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "features_to_ohe = [\n",
    "    'titleTypeEncoded',\n",
    "    'isAdult'\n",
    "] + list(unique_genres)\n",
    "\n",
    "ohe = OneHotEncoder()\n",
    "ohe.fit(X_train[features_to_ohe])\n",
    "\n",
    "X_train_ohe = ohe.transform(X_train[features_to_ohe])\n",
    "X_valid_ohe = ohe.transform(X_valid[features_to_ohe])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_holdout_ohe = ohe.transform(holdout[features_to_ohe])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Обработка названий произведений и \"имен\" авторов\n",
    "Имена взял в кавычки, т. к. хоть это и строки, на самом деле это идентификаторы в строковой форме."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "title_vectorizer = CountVectorizer(\n",
    "    ngram_range=(1,3),\n",
    "    max_features=160000,\n",
    "    stop_words='english')\n",
    "\n",
    "title_vectorizer.fit(X_train['primaryTitle'])\n",
    "\n",
    "X_train_title_cv = title_vectorizer.transform(X_train['primaryTitle'])\n",
    "X_valid_title_cv = title_vectorizer.transform(X_valid['primaryTitle'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "directors_vectorizer = CountVectorizer(\n",
    "    ngram_range=(1,2), # Возможно, совместно определенные пары выдают наилучший результат\n",
    "    max_features=160000)\n",
    "\n",
    "directors_vectorizer.fit(X_train['directors'])\n",
    "\n",
    "X_train_dir_cv = directors_vectorizer.transform(X_train['directors'])\n",
    "X_valid_dir_cv = directors_vectorizer.transform(X_valid['directors'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "writers_vectorizer = CountVectorizer(\n",
    "    ngram_range=(1,2), # Аналогичная гипотеза про пары\n",
    "    max_features=160000)\n",
    "\n",
    "writers_vectorizer.fit(X_train['writers'])\n",
    "\n",
    "X_train_wrt_cv = writers_vectorizer.transform(X_train['writers'])\n",
    "X_valid_wrt_cv = writers_vectorizer.transform(X_valid['writers'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "principals_vectorizer = CountVectorizer(\n",
    "    ngram_range=(1,3), # Похожая гипотеза, на этот раз, включая и тройки\n",
    "    max_features=240000)\n",
    "\n",
    "principals_vectorizer.fit(X_train['principals'])\n",
    "\n",
    "X_train_ppl_cv = principals_vectorizer.transform(X_train['principals'])\n",
    "X_valid_ppl_cv = principals_vectorizer.transform(X_valid['principals'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_holdout_title_cv = title_vectorizer.transform(holdout['primaryTitle'])\n",
    "X_holdout_dir_cv = directors_vectorizer.transform(holdout['directors'])\n",
    "X_holdout_wrt_cv = writers_vectorizer.transform(holdout['writers'])\n",
    "X_holdout_ppl_cv = principals_vectorizer.transform(holdout['principals'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь соберем все вместе."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_processed = hstack((\n",
    "    X_train_scaled,\n",
    "    X_train_ohe,\n",
    "    X_train_title_cv,\n",
    "    X_train_dir_cv,\n",
    "    X_train_wrt_cv,\n",
    "    X_train_ppl_cv\n",
    "))\n",
    "\n",
    "valid_processed = hstack((\n",
    "    X_valid_scaled,\n",
    "    X_valid_ohe,\n",
    "    X_valid_title_cv,\n",
    "    X_valid_dir_cv,\n",
    "    X_valid_wrt_cv,\n",
    "    X_valid_ppl_cv\n",
    "))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "holdout_processed = hstack((\n",
    "    X_holdout_scaled,\n",
    "    X_holdout_ohe,\n",
    "    X_holdout_title_cv,\n",
    "    X_holdout_dir_cv,\n",
    "    X_holdout_wrt_cv,\n",
    "    X_holdout_ppl_cv\n",
    "))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Кросс-валидация и настройка гиперпараметров модели"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sgd_reg = SGDRegressor(alpha=0.0001,\n",
    "                       loss='squared_loss',\n",
    "                       max_iter=300,\n",
    "                       tol=1e-3,\n",
    "                       random_state=17)\n",
    "sgd_reg.fit(train_processed, y_train)\n",
    "y_pred = sgd_reg.predict(valid_processed);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mse = mean_squared_error(y_valid, y_pred)\n",
    "mse_sqrt = mse ** 0.5\n",
    "mse_sqrt_bc_reverse = reverse_boxcox(mse_sqrt, bc_lmbda)\n",
    "print(\"MSE: {:.3f}\\n\\\n",
    "Square root of MSE: {:.3f}\\n\\\n",
    "Same with Reversed Box-Cox Transformation: {:.3f}\".format(\n",
    "    mse,\n",
    "    mse_sqrt,\n",
    "    mse_sqrt_bc_reverse\n",
    "))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видно, что модель (с параметрами по умолчанию) вполне может ошибиться на 4 балла (при том, что расстояние между максимальной оценкой и минимальной — всего 10 баллов). Посмотрим, поможет ли кросс-валидация.\n",
    "\n",
    "Я использую обычное KFold разбиение на 6 фолдов. Данных достаточно много, чтобы качество не сильно \"скакало\" на таком количестве фолдов. `shuffle=True`, т. к., как уже обсудили выше, мы не сильно зависим от времени.\n",
    "\n",
    "На мой взгляд, у SGD в данном случае имеет смысл перебирать только гиперпараметр регуляризации, `alpha`. Его возможные значения подбираю в интервале от `1e-5` до `1e1`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "kf = KFold(6, shuffle=True, random_state=17)\n",
    "params = {'alpha': np.logspace(-5, 1, 7)}\n",
    "\n",
    "gscv = GridSearchCV(sgd_reg,\n",
    "                    params,\n",
    "                    scoring='neg_mean_squared_error',\n",
    "                    cv=kf)\n",
    "gscv.fit(train_processed, y_train);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(gscv.best_params_)\n",
    "print(gscv.best_score_)\n",
    "print(reverse_boxcox((-1 * gscv.best_score_) ** 0.5, bc_lmbda))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно, кросс-валидация сильно не помогла."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Построение кривых валидации и обучения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "alphas = np.logspace(-5, 1, 7)\n",
    "\n",
    "train_scores_vc, test_scores_vc = validation_curve(gscv.best_estimator_,\n",
    "                                             train_processed,\n",
    "                                             y_train,\n",
    "                                             'alpha',\n",
    "                                             alphas,\n",
    "                                             cv=kf,\n",
    "                                             scoring='neg_mean_squared_error')\n",
    "\n",
    "train_sizes = np.linspace(0.1, 1, 7)\n",
    "\n",
    "train_size, train_scores_lc, test_scores_lc = learning_curve(gscv.best_estimator_,\n",
    "                                                       train_processed,\n",
    "                                                       y_train,\n",
    "                                                       train_sizes=train_sizes,\n",
    "                                                       cv=kf,\n",
    "                                                       scoring='neg_mean_squared_error')\n",
    "# ~14 min"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_with_err(x, data, **kwargs):\n",
    "    mu, std = data.mean(1), data.std(1)\n",
    "    lines = plt.plot(x, mu, '-', **kwargs)\n",
    "    plt.fill_between(x, mu - std, mu + std, edgecolor='none',\n",
    "    facecolor=lines[0].get_color(), alpha=0.2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(20, 7))\n",
    "\n",
    "plt.subplot(121)\n",
    "plot_with_err(alphas, train_scores_vc, label='training scores')\n",
    "plot_with_err(alphas, test_scores_vc, label='validation scores')\n",
    "plt.xlabel(r'$\\alpha$')\n",
    "plt.xscale('log')\n",
    "plt.ylabel('Neg MSE')\n",
    "plt.legend()\n",
    "\n",
    "plt.subplot(122)\n",
    "plot_with_err(train_size, train_scores_lc, label='training scores')\n",
    "plot_with_err(train_size, test_scores_lc, label='validation scores')\n",
    "plt.xlabel('Training Set Size')\n",
    "plt.ylabel('Neg MSE')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Интерпретация\n",
    "\n",
    "1. Чем меньше alpha, тем лучше скор, но и выше разброс. Конечно, выбирать значение регуляризации, находящееся на границе перебора через GridSearch, не рекомендуется. Но и подбирать слишком маленькое alpha тоже не рекомендуется, чтобы модель не переобучилась.\n",
    "\n",
    "2. В начале learning curve мы видим небольшой пик. Я думаю, это свидетельствует о переобучении, так как ошибки в трейне и на валидации отличаются довольно сильно по сравнению с правой частью графика. А справа разница между скорами снижается и стабилизируется на более-менее постоянном значении. Схождение могло бы свидетельствовать о недообучении. Из этого можно сделать вывод, что можно попробовать добавить больше данных без изменения модели, так как значения скоров (пока) не сошлись."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "\n",
    "train_complete = vstack((train_processed,\n",
    "                         valid_processed))\n",
    "\n",
    "gscv.best_estimator_.fit(train_complete, y_train_complete)\n",
    "\n",
    "y_holdout_pred = gscv.best_estimator_.predict(holdout_processed)\n",
    "\n",
    "# ~30s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Результаты на тестовой выборке"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mse_h = mean_squared_error(y_holdout_true, y_holdout_pred)\n",
    "mse_h_sqrt = mse_h ** 0.5\n",
    "mse_h_sqrt_bc_reverse = reverse_boxcox(mse_h_sqrt, bc_lmbda)\n",
    "print(\"MSE: {:.3f}\\n\\\n",
    "Square root of MSE: {:.3f}\\n\\\n",
    "Same with Reversed Box-Cox Transformation: {:.3f}\".format(\n",
    "    mse_h,\n",
    "    mse_h_sqrt,\n",
    "    mse_h_sqrt_bc_reverse\n",
    "))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "После того, как мы опробовали модель на holdout выборке, видно, что качество результата немного упало. Если на валидации ошибка могла быть в среднем около 3.92 балла, то для holdout это значение составляет уже 4.773. Думаю, частично это можно объяснить тем, что распределение holdout'а немного отличается от той части, на которой производился перебор альфы."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ценность решения\n",
    "Из результатов тестов видно, что если мы будем всем фильмам предсказывать оценку 5.5 баллов `((10 - 1) / 2) + 1`, то качество такого предсказания будет не сильно отличаться от качества работы построенной модели. Поэтому можно заключить, что модель в таком виде не представляет такой ценности, которую хотелось бы создать с учетом целей, обозначенных во введении.\n",
    "\n",
    "### Возможности применения\n",
    "В силу крайне низкого качества модели ей остается шуточное применение. В данных из `./data/name.basics.tsv/data.tsv` можно найти идентификаторы интересующих вас режиссеров, сценаристов и других профессионалов. Придумать заголовок для вашего будущего фильма. И подставить эти значения на вход модели, чтобы узнать, как хорошо ее примут зрители :)\n",
    "\n",
    "### Дальнейшие пути развития и улучшения решения\n",
    "Несмотря на текущие результаты, есть надежда, что поставленных целей все же можно достичь с помощью доработок модели. Для дальнейшего усовершенствования модели имеет смысл поработать следующие шаги.\n",
    "1. Из name.basics позаимствовать информацию о других работах каждого участника (колонка `knownForTitles`), и о рейтингах этих работ (при условии, конечно, их наличия в обучающей выборке, чтобы избежать ликов). Это имеет смысл, если верна гипотеза, что у каждого человека работа получается примерно одного уровня (что далеко не всегда верно).\n",
    "2. На основе рейтингов других работ каждого участника и их года релиза можно построить динамику и использовать ее, а не среднее арифметическое из п. 1.\n",
    "3. Кроме того, имеет смысл считать статистику от таких рейтингов для всей команды оцениваемого фильма. Например, насколько сильно отличаются прошлые успехи членов команды. Например, если отличия небольшие и при этом рейтинги были высокие, интуитивно кажется, что и на этот раз все будет хорошо.\n",
    "4. Возможно, имеет смысл парсить открытые источники для получения информации о бюджете на создание фильма и о кассовых сборах. Конечно, информацию о кассовых сборах использовать в такой модели не очень интересно, ведь эта информация неизвестна для фильмов, которые только планируются. Зато информацию о бюджете использовать разумно.\n",
    "5. Наконец, было бы здорово использовать информацию о доминирующих цветах в палитре фильма."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Лицензионная информация об использованных данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Information courtesy of<br/>\n",
    "IMDb<br/>\n",
    "(http://www.imdb.com).<br/>\n",
    "Used with permission.<br/>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
